Formula1 findings: blank-page boot, OData read contract, log levels, and four data-loss fixes - #120
Merged
Merged
Conversation
…re it
Every page in a locally-run app rendered as a black screen, intermittently and
at the same mxcli version. `run --local` bundles the browser client at step 5b
and it succeeds; then the runtime boot runs Gradle `clean-custom-classes compile
package`, whose package pass repopulates deployment/web and takes dist/ with it.
The bundle was deleted 51 seconds after the same command wrote it.
It only bites when Gradle has work to do — a new Java action, a full recompile —
which is why an app boots fine for weeks and then stops with nothing changed.
Nothing reported it. `mxcli check` passes, the build succeeds, the runtime log
is quiet, `curl /` returns 200 with a valid HTML shell, and every OData service
answers. Only a browser sees it.
A pre-condition established before a step that rewrites the same directory is
not a post-condition, so the bundle is now verified after the boot:
- WebClientBundled / EnsureWebClientBundle re-bundle only when it is gone,
and say so. When Gradle had nothing to do this is a stat.
- Re-ordering the bundle to after the boot instead was rejected: it would
leave the app reachable-but-blank for ~30s on every cold start.
- A failed re-bundle warns and leaves the runtime up — the app's services
still work, only the browser is broken — and names the blank page.
`mxcli test --local` boots the same way and destroys the bundle too, which is
why a test run between a boot and a browser looked like a rendering bug. Tests
are headless, so that path reports the loss and the remedy instead of spending
~30s on a loop whose point is two seconds.
Both controls run: with the guard never firing the wipe test fails, and with it
always firing the survivor test fails.
mxcli-formula1 §35
…citly
A grid drill-down written as
linkbutton btnWeekend (Caption: 'Weekend',
Action: SHOW_PAGE Mod.Race_Weekend(Race: $currentObject))
described back as `show_page Mod.Race_Weekend`, with no argument.
The mapping was never missing from the model. mxcli writes a page action's
ParameterMappings as an empty array on purpose: Studio Pro infers the current
row object from the enclosing widget, and an explicit mapping whose Argument is
"$currentObject" is rejected as CE0115 (mendixlabs#296). That decision stands. What was
missing is its other half — DESCRIBE read only explicit mappings, so the
implicit argument had nowhere to come from. The writer's comment claimed
DESCRIBE recovered it; it did not, and now it does.
The cost is not cosmetic. DESCRIBE is what you reach for once you have stopped
trusting the model, so its output arrives already looking like a conclusion: the
mapping was dropped, that is why the page gets an empty object. It is a
plausible, wrong answer at the worst possible moment, and it cost three
debugging cycles replacing a button that was correct.
Recovery reads the target page's own declared parameters, since that is where
the information lives. Both ends are guarded: an explicit mapping still wins,
and an unresolvable page yields no arguments rather than invented ones.
Control: with recovery removed the test reproduces the reported output verbatim.
mxcli-formula1 §39
`create or modify odata service` silently revoked the service's access, so the next build failed with "At least one allowed role must be selected for the published OData service to be accessible." Grants are made by a separate statement and cannot be re-stated in the create script, so nothing in the script could put them back — only a manual re-grant, until the next modify. serializePublishedODataService never wrote AllowedModuleRoles. The document is serialized wholesale and written with updateUnit, so a field the serializer omits is not left alone: it is deleted. A wholesale re-serialization makes the writer's field list a data-retention policy, and this one was missing an entry. The array uses storage marker 1 (BY_NAME references) — the same shape the working GRANT path writes via makeMendixStringArray, rather than a marker reasoned out from an unrelated type. An earlier pass looked for this loss at model level, found the grants present and correctly carried through the modify branch, and recorded it as "reported but does not reproduce". That conclusion was wrong: the loss only exists after the round trip to BSON, which a model-level check cannot see. The carry-through guard added then was a no-op; it is kept for a caller that clears the slice, and its comment now says which layer actually held the bug. Control: with the field omitted again the test reproduces the empty-grants document. mxcli-formula1 §26
…es them
The skills are embedded in mxcli and written exactly once, by `mxcli init`, so
upgrading the binary did nothing to them: a project initialised on Monday still
served Monday's guidance from Tuesday's mxcli, with no warning. Confirmed with a
binary rebuilt at 12:05 beside skills stamped the previous day.
Stale guidance is worse than missing guidance — an agent reads it with the same
confidence either way, and the point of shipping skills inside the binary is
that the two versions agree.
Fixed where the files are consumed rather than where they are authored: the
SessionStart bootstrap script already runs on every session and can fetch the
binary, so it is the one place guaranteed to execute immediately before an agent
reads them. It now runs `mxcli init --sync-skills`, a new flag that refreshes
only .ai-context/skills/ and exits.
Two properties make an every-session job acceptable:
- it writes only the files that differ, so mtimes keep meaning "when did this
guidance last move" (a test asserts an unchanged skill is not rewritten);
- it is silent when the project is already current.
It is never fatal — a skills refresh must not block a session. A test asserts
the bootstrap calls it before the exec'd setup, since a step ordered after an
exec never runs.
Control: with write-once restored, the stale file survives the sync.
mxcli-formula1 §16
…ng true `publish entity … (TopSupported: No)` parsed, described back as No, and published as Yes: serializeEntitySet hardcoded all three QueryOptions to true and never read the *bool fields the model already carried. The AST, model and DESCRIBE halves of the feature had shipped without the writer half — which is exactly the shape a DESCRIBE round-trip test cannot catch, since DESCRIBE reads the model, not the published document. For a microflow-backed resource this claim is load-bearing rather than decorative. Mendix applies no query options to a read-microflow resource: it hands the request to the microflow and returns what comes back. The annotation is therefore the only thing a client has to go on, and an over-claim is not cosmetic — a client that believes $top works reads a whole collection as though it were a page. nil keeps Mendix's own default of true; only an explicit false opts out, which is what the model's comment already promised. Control: with the constants restored the test reproduces the over-claim. mxcli-formula1 §20
…omises
A published OData resource backed by a read microflow could silently return
the wrong thing, twice over:
- `?$top=5` returned the whole collection with a 200, because Mendix applies
NO query options to a read-microflow resource — it hands over the request
and returns what comes back. TopSupported/SkipSupported describe the
microflow, not the platform, and default to true when unspecified.
- a client re-reading a row it holds sends `?$filter=key eq '…'` unprompted.
With no branch for it the request falls through to the collection default
and the client adopts the FIRST row as that object's identity. No error:
well-formed request, valid collection, correct $count, 200.
Both are promises the service makes on the microflow's behalf, and nothing
checked either. MDL-ODATA02 flags a declared KEY the microflow cannot answer;
MDL-ODATA03 flags capabilities it cannot implement.
The read path has no other way to be safe. Unlike an OData action or an
insert/update/delete microflow, a read microflow has no System.HttpResponse
parameter and cannot answer 400, so declaring `TopSupported: No` is its only
substitute for the refusal it cannot send. That contract is now documented per
capability in odata-data-sharing.md and in `mxcli syntax odata.publish`, which
neither covered before.
Both rules fire on one provable condition — the microflow takes no
System.HttpRequest parameter, so it cannot see a key or a query option at all.
A microflow that does take it gets the benefit of the doubt: proving which
options it parses needs real analysis, and a rule that guesses gets switched
off.
The rule shipped dead once during development: the visitor stores ReadMode as
`MICROFLOW Module.Name` upper-cased and the prefix match was case-sensitive.
Caught by running it against a real script rather than a hand-built AST, and
now pinned by its own test.
Swept mdl-examples/ and the skill MDL blocks for false positives: 0 failures.
mxcli-formula1 §37, §20 (suggested issues 2 and 3)
Everything logs at INFO, so the detail you need is usually not in the log at
all — and raising the whole runtime to TRACE is unusable on a busy app. The
M2EE admin API has exposed per-node levels all along; nothing in mxcli drove
them.
mxcli log list [--filter x] [--json]
mxcli log set <node> <level>
mxcli log set A=TRACE B=DEBUG # one admin call, applied together
This was proposed as `mxcli odata trace`, for "what is my published resource
being asked?". It is deliberately not that: set_log_level takes a LIST of nodes
and the runtime reports 57 of them, so the primitive is subsystem-agnostic and a
per-subsystem command would have wrapped it one subsystem at a time. The OData
knowledge lives in the skill instead — including the finding that there is NO
log node for a published OData service (ODataConsume is the client side), so
that particular question still needs a LOG in the read microflow. That gap is
Mendix's.
Every API fact was probed against a live 11.12.1 runtime, because the HTTP
response for an AdminException says only "See logging output for details" — the
real message is in the runtime log:
- get_log_settings requires one of node/subscriber/sort in params
- sort accepts exactly "node" and "subscriber"
- set_log_level takes {"nodes":[{name,level}],"force":bool}
- force means "allow a node that does not exist yet", and permanently
registers the name — so it is opt-in and an unknown node is an error by
default, making a typo an error rather than a setting that never applies
- an invalid level is refused
A level typo is caught locally with the valid set named, rather than becoming an
AdminException whose detail the caller never sees. "Cannot reach the admin API"
is distinguished from "the runtime refused the request", so the connection hint
does not appear on an unknown-node error and bury the sentence that matters.
Verified end-to-end against a booted runtime: both argument forms, multi-node in
one call, a typo refused, --force accepted, and an unreachable port.
mxcli-formula1 suggested issue 4
The previous commit claimed there is no log node for a published OData service, and that "what is my published resource being asked?" therefore still needed a LOG in the read microflow. That was wrong. `OData Publish` — with a space — exists whenever the project publishes a service, and at TRACE it logs the full incoming URI: TRACE - OData Publish: Incoming request from 127.0.0.1: GET .../Rows?$top=5&$filter=rowKey eq 'abc' DEBUG - OData Publish: Responding to client with status code 400. which is precisely the question the command was built for. The mistake is worth naming, because it is the same one in a new place: the node list is a property of the APP, not of Mendix. Nodes appear once something registers them. The first probe ran against a project with zero OData services, saw 57 nodes and no publish node, and generalised. Adding one service makes it 58. Enumerating a platform's capabilities from one sample app and calling the result "Mendix does not have this" is exactly what `log list` against your own app is for — the command was right, the conclusion drawn beside it was not. The same probe also showed Mendix rejecting `$filter` on a property not declared Filterable, with 400 "Property 'x' is non-filterable", BEFORE the read microflow runs — so the platform does enforce declared filterability, which is more than §20 credited it with. What it still does not do is apply $top/$skip/$orderby for a read-microflow resource. Corrected in analyze-runtime.md (node table + a section on the publish node), odata-data-sharing.md, the command's own help, and the fix-issue row. mxcli-formula1 suggested issue 4
…logged into Under --hub the runtime boots with the public https root URL, so it marks its session cookies Secure and prefixes them __Host-. A headless browser reaching the app over http then cannot hold a session, every screenshot silently shows the login page, and rendering defects survive every other check. The login browser context now sends X-Forwarded-Proto: http when the target is http. On Mendix 10.24+ that header takes precedence over ApplicationRootUrl, so the runtime drops both Secure and the __Host- prefix. This is accurate rather than a workaround — the request genuinely is http — it is a no-op when the root URL is already http, and real users arriving over https through the hub are unaffected. Verified at the layer the bug lives in, against a live 11.12.1 runtime booted with an https root URL: the captured Playwright storage state goes from __Host-XASSESSIONID(secure=true) to XASSESSIONID(secure=false). The reported cause did NOT reproduce as stated, which is worth recording. On 127.0.0.1 an https root URL blocks nothing: loopback is a trustworthy origin, so Chromium accepts Secure and __Host- cookies there, and the app rendered clean with no console errors and no failed requests. The mechanism is real only from a NON-loopback http origin — a container hostname, a LAN address — where the origin is not trustworthy and the session cannot be held at all. The fix ships because it is correct and free, not because it was shown to repair the reported symptom. mxcli-formula1 §38 / suggested issue 7
…t in
declare $Msg String = 'a' +
-- explain the second half
'b';
stored the comment INSIDE the expression, and the build failed CE0117
"Error(s) in expression". Nothing before mxbuild objected: `mxcli check` passed
and DESCRIBE round-tripped the comment back out.
extractOriginalText reads the raw input stream between two token positions.
That is exactly what preserves an expression's spacing — and it also drags in
every token the lexer sent to a hidden channel. MDL's `--` and `/* */` are
`-> skip`, so they never appear in ctx.GetText() and always appear in a source
slice. Mendix expressions have neither form.
Expression sites now go through extractExpressionText, which strips MDL comments
first. Two properties matter:
- a comment becomes whitespace, never nothing, so `1 --c\n+ 2` cannot become
`1+ 2` and `'a'--c\n'b'` cannot weld into one token;
- single-quoted strings are respected, because a Mendix string may legitimately
contain `--` or `/*` and stripping those would corrupt the value. The `''`
escape that keeps a string open is handled too.
OQL keeps extractOriginalText on purpose: `--` is legitimate SQL comment syntax
in a view entity's query, and stripping it would change a different language's
meaning.
Verified end to end against mxbuild 11.12.1 on the same project and script: 1
error before, 0 after. The control matters here — with extractExpressionText
bypassed the unit tests still passed, because they exercise the function rather
than the wiring, and only the mxbuild run showed the call sites were converted.
Swept mdl-examples/ and the 189 checkable skill MDL blocks: 0 failures.
mxcli-formula1 §34 / suggested issue 11
CI caught this: the doctype gate failed on 10-odata-examples.mdl with
[CE6585] "Published entity 'OdTest.LiveRow' must have a key defined."
at Published OData service 'OdTest.BulkAPI'
The example demonstrated an "honest contract" resource by omitting the KEY —
the alternative MDL-ODATA02 offers to answering a key lookup. Mendix does not
permit that: a published entity must have a key. So the rule shipped in this
branch was recommending something the platform rejects, and the example
demonstrated it.
Corrected in all three places the claim appeared: the rule's suggestion, the
example, and odata-data-sharing.md. The resulting position is sharper than the
one it replaces — query options you may decline, the key you may not. A
microflow-backed resource whose rows a client can hold must answer the key
lookup; there is no opt-out.
I validated that example with `mxcli check`, which is parse-only and cannot see
a CE code, rather than the integration gate — the same mistake made earlier in
this branch on 18-folder-examples.mdl, and already written down in fix-issue.md
at the time. The new symptom row states the rule plainly: a change to
mdl-examples/doctype-tests/ is not done until the gate has run on it.
mxcli-formula1 §37 follow-up
This was referenced Aug 9, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Ten commits from the latest mxcli-formula1 findings. Every fix has a control that reproduces the reported symptom before the change; the ones whose symptom lives in a built app were verified against mxbuild or a booted runtime, not just in unit tests.
Critical: every page was a black screen
run --localdeleted the browser bundle it had just built (§35). Step 5b bundles the web client; the runtime boot then runs Gradleclean-custom-classes compile package, whose package pass repopulatesdeployment/weband takesdist/with it — 51 seconds after the same command wrote it. It only bites when Gradle has work to do, which is why an app boots fine for weeks and then stops with nothing changed.Nothing reported it:
mxcli checkpassed, the build succeeded, the runtime log was quiet,curl /returned 200 with a valid HTML shell, and every OData service answered. Only a browser saw it.A pre-condition established before a step that rewrites the same directory is not a post-condition, so the bundle is now verified after the boot. When Gradle had nothing to do that check is a
stat.mxcli test --localdestroys the bundle too; tests are headless, so that path reports the loss and the remedy rather than spending ~30s on a two-second loop.Data loss in what we write
create or modify(§26), so the next build failed "At least one allowed role must be selected".serializePublishedODataServicenever wroteAllowedModuleRoles, and the document is serialized wholesale — a field the serializer omits is not left alone, it is deleted. An earlier pass looked for this at model level, found the grants correctly carried, and recorded "does not reproduce". That was wrong: the loss only exists after the round trip to BSON. The comment now says which layer held the bug.DESCRIBE PAGEdropped a page-parameter mapping (§39). mxcli stores it implicitly on purpose (an explicit$currentObjectargument is CE0115), but DESCRIBE had no compensating recovery — and a comment claimed it did. Costly because DESCRIBE is what you reach for once you have stopped trusting the model, so its output arrives already looking like a conclusion.extractOriginalTextreads the raw source between token positions — which preserves spacing and drags in every hidden-channel token. Comments become whitespace (never nothing, or tokens weld), single-quoted strings are respected, and OQL keeps the old helper because--is legitimate SQL there.publish entity … (TopSupported: No)published asYes(§20) — the writer hardcoded all threeQueryOptions. The shape a DESCRIBE round-trip test cannot catch, since DESCRIBE reads the model rather than the published document.The OData read contract, checked
Two new rules (MDL-ODATA02/03) for microflow-backed resources, from the bug that cost fifteen restart cycles: a client holding a row re-reads it by key unprompted, and a resource that answers with its collection default teaches the client the wrong identity for an object already on screen — valid collection, correct
$count, 200, no error.A read microflow cannot answer 400: unlike an OData action or an insert/update/delete microflow, the read capability has no
System.HttpResponseparameter. Its contract is therefore declarative, and declaringTopSupported: Nois its only substitute for the refusal it cannot send. That contract is now documented per capability inodata-data-sharing.mdandmxcli syntax odata.publish, which neither covered before.Both rules fire on one provable condition — the microflow takes no
System.HttpRequestparameter, so it cannot see a key or a query option at all. A microflow that does take it gets the benefit of the doubt. Sweptmdl-examples/and 189 skill MDL blocks: 0 false positives.New:
mxcli logProposed as
mxcli odata trace; built generic instead, becauseset_log_leveltakes a list of nodes. Every API fact was probed against a live runtime — the HTTP response for a rejected admin call says only "See logging output for details", so the real message is in the runtime log.forcemeans "allow a node that does not exist yet" and permanently registers the name, so it is opt-in and an unknown node is an error by default.The node list belongs to the app, not to Mendix:
OData Publish(with a space) appears only when the project publishes a service, and at TRACE it logs the full incoming URI — the question that motivated the whole thing. A first pass concluded no such node existed, from a project that had none; the correction commit records that trap.Also
.ai-context/skills/now follows a binary upgrade (§16). Fixed where the files are consumed — the SessionStart bootstrap — writing only what differs so mtimes keep meaning, and silent when current.Securecookies work on127.0.0.1. The mechanism is real only from a non-loopback origin. Shipped because the header is correct and free, not because it was shown to repair the symptom.Not included
MENU ITEM … ICON(§36) is deliberately absent. The metamodel offers onlyPages$GlyphIcon/IconCollectionIcon/ImageIconwith noMenus$…Icon,GlyphIconcarries a numericCoderather than a name, and no project here has a menu icon to copy. Guessing a polymorphic child's$Typeproduces a document mxbuild accepts and Studio Pro cannot open — worse than the missing feature. A Studio Pro reference would unblock it.Verification
mxcli logend-to-end, cookie flags,OData Publishat TRACEmdl-examples/+ 189 skill MDL blocks, 0 failures🤖 Generated with Claude Code
https://claude.ai/code/session_01JXnEgoc2NQP1Y2TWMCMXC4
Generated by Claude Code